/* 
 * Copyright (c) 2009 Seth Wessitsh (seth@wessitsh.com)
 * Licensed under the MIT (MIT-LICENSE.txt) licence.
 */

/**
	The JSON.schema.err namespace contains error classes which may occur in JSON Schemas
	as well as facilities for localizing the error instances.
	
	Currently the error classes are:
		SchemaError - Base error class
			RequiredTypeError - Something requires that a certain type be allowed in the schema
			TypeError - Something must be of a particular type
			SubTypeError - Something must be a subtype of something else
			IntroductionError - Something introduced into a decendent schema is not allowed
			InvalidValueError - Something has a value which is the proper type but not valid
	
	The localization classes are:
		ErrorLocalizer - Base localizer class, provides default localization capabilities
			SubTypeErrorLocalizer - Specialized localizer for SubTypeError, allows display of causes

	The localization functions are:
		localize
			Automatically creates an appropriate localization class for the error
			provided and returns a message for that error in the language specified
	
	The localication classes/functions use the JSON.schema.err.messages hash in order to retrive
	localized error messages.

	For more information on each of these classes/functions see their interface documentation.
 */
var JSON = JSON ? JSON : {};
JSON.schema = JSON.schema ? JSON.schema : {};
JSON.path = JSON.path ? JSON.path : function noJSONPathFound() { return ["JSON.path not found"]; };

JSON.schema.err = {
	/**
		@interface {
			id: "JSON.schema.err.SchemaError",

			description: "
				The base class for all Schema errors.
				For the most part this is simply a tagging class but also provides a
				uniform interface for subclasses. ",
			
			parameters: [
				{ name: "code", type: "string",
					description: "
						A unique code for the particular error which occurred. This code is used as a 
						key to retrieve error messages from JSON.schema.err.messages[<language>].  
						Where <language> would be replaced by the appropriate language code (en by 
						default). " },
				
				{ name: "data", type: "object",
					description: "
						An object which provides a collection of data about the error.  No specific 
						structure or data is required, however, the error messages retrieve data 
						relevant to the error so each error class adopts a specific convention."} }
		}
	 */
	SchemaError: function SchemaError(code, data) { this.name = "SchemaError"; this.code = code; this.data = data; },
	/**
		@interface {
			id: "JSON.schema.err.RequiredTypeError",
			extends: "JSON.schema.err.SchemaError",

			description: "
				An error class representing the set of errors in which a particular portion of a
				schema required the schema hierarchy allow a particular type, yet it did not.  
				I.e., part of a schema required that some type be allowed but that type was not 
				allowed.
				
				E.g., {type: "object", minimum: 5} produces a RequiredTypeError as the minimum
				restriction requires that the type number be allowed. ",
			
			parameters: [
				{ name: "data", type: "object",
					description: "An object with information relevant to the error",
					properties: {
						current: { type: "object",
							properties: {
								schema: { type: "JSONSchema",
									description: "
										The schema we were processing when the error occurred" },

								path: { type: "string",
									description: "
										A JSON.path with the schema property as its root. This indicates the part of the 
										schema that required the type given in requires" },

								value: { type: "any",
									description: "The value of the property at path" } } },

						requires: { type: "object",
							description: "An object with information about what was required" 

							properties: {
								value: { type: "string",
									description: "A string indicating the name of the type required" } } } } } ] }
	*/
	RequiredTypeError: function RequiredTypeError(code, data) { this._ctor(code, data); this.name = "RequiredTypeError"; },
	/**
		@interface {
			id: "JSON.schema.err.TypeError",
			extends: "JSON.schema.err.SchemaError",

			description: "
				An error class representing the set of errors in which a particular portion of a
				schema needed to have a particular type yet did not.
				I.e., part of a schema should have had type X but did not.
				
				E.g., {type: "object", minimum: "abc"} produces a TypeError as the minimum
				restriction should be an integer ",
			
			parameters: [
				{ name: "data", type: "object",
					description: "An object with information relevant to the error",

					properties: {
						current: { type: "object",
							properties: {
								schema: { type: "JSONSchema",
									description: "
										The schema we were processing when the error occurred" },

								path: { type: "string",
									description: "
										A JSON.path with the schema property as its root.  This indicates the part of 
										the schema which had the inappropriate type" },

								value: { type: "any",
									description: "The value of the property at data.current.path" } } },

						requires: { type: "object",
							properties: {
								value: { type: "string",
									description: 
										"A string indicating the type/s which were required" } } } } } ] }
	*/
	TypeError: function TypeError(code, data) { this._ctor(code, data); this.name = "TypeError"; },
	/**
		@interface {
			id: "JSON.schema.err.SubTypeError",
			extends: "JSON.schema.err.SchemaError",

			description: "
				An error class representing the set of errors where a particular portion of a 
				schema needed to be a subtype of another portion of the same schema, or some 
				schema in the hierarchy, yet was not.
				I.e., part of a schema should have been a subtype of some other part.
				
				E.g., {extends: "array", type: "object"} produces a SubTypeError as 
				the value of the type restriction must be a subtype of the value of the extends
				restriction",
			
			parameters: [
				{ name: "data", type: "object",
					description: "An object with information relevant to the error",
					properties: {
						current: { type: "object",
							properties: {
								schema: { type: "JSONSchema",
									description: "
										The schema we were processing when the error occurred" },

								path: { type: "string",
									description: "
										A JSON.path with the schema property as its root.  This indicates the part of 
										data.current.schema which was not a subtype of data.conflictsWith.path" },

								value: { type: "any",
									description: "The value of the property at data.current.path" } } },

						conflictsWith: { type: "object",
							properties: {
								schema: { type: "JSONSchema",
									description: "
										A schema, part of which data.current.path should have been a subtype of" },

								path: { type: "string",
									description: "
										A JSON.path with data.current.schema as its root.  This indicates the part of
										data.conflictsWith.schema that data.current.path should have been a subtype of" },

								value: { type: "any",
									description: 
										"The value at data.conflictsWith.path" } } }

						causedBy: { type: "array",
							description: "
								A, possibly empty, set of errors which caused data.current.path to not be a SubType of 
								data.conflictsWith.path",

							items: {type: "JSON.schema.err.SchemaError"} } } } ] }
	*/
	SubTypeError: function SubTypeError(code, data) { this._ctor(code, data); this.name = "SubTypeError"; },
	/**
		@interface {
			id: "JSON.schema.err.IntroductionError",
			extends: "JSON.schema.err.SchemaError",

			description: "
				An error class representing the set of errors where something was introduced into 
				a particular portion of a schema and this introduction was disallowed.
				I.e., part of a schema didn't allow anything new to be added, yet something was.
				
				E.g., {
					extends: {
						properties: {a: {}}, 
						additionalProperties: false
					},
					type: "object",
					properties: {b: {}}
				} 
				produces an IntroductionError because the extended type does not allow any new properties
				to be introduced, yet property b was. ",
			
			parameters: [
				{ name: "data", type: "object",
					description: "An object with information relevant to the error",
					properties: {
						current: { type: "object",
							properties: {
								schema: { type: "JSONSchema",
									description: "
										The schema we were processing when the error occurred" },

								path: { type: "string",
									description: "
										A JSON.path with the schema property as its root.  This indicates the part of 
										data.current.schema which should not have been introduced. " },

								value: { type: "any",
									description: "The value of the property at data.current.path" } } },

						conflictsWith: { type: "object",
							properties: {
								schema: { type: "JSONSchema",
									description: "
										A schema, part of which precludes the possibility of introducting something
										in any subschemas." },

								path: { type: "string",
									description: "
										A JSON.path with data.current.schema as its root.  This indicates the part of
										data.conflictsWith.schema which indicates an introduction was not valid" },

								value: { type: "any",
									description: 
										"The value at data.conflictsWith.path" } } } } } ] }
	*/
	IntroductionError: function IntroductionError(code, data) { this._ctor(code, data); this.name = "IntroductionError"; },
	/**
		@interface {
			id: "JSON.schema.err.InvalidValueError",
			extends: "JSON.schema.err.SchemaError",

			description: "
				An error class representing the set of errors where a particular part of a schema
				should not have had a particular value.
				I.e., part of a schema shouldn't have had the value that it did.
				
				E.g., { extends: { minimum: 5 }, minimum: 3 } produces an InvalidValueError because
				the extended type requires a larger minimum than the extending type. ",

			parameters: [
				{ name: "data", type: "object",
					description: "An object with information relevant to the error",
					properties: {
						current: { type: "object",
							properties: {
								schema: { type: "JSONSchema",
									description: "
										The schema we were processing when the error occurred" },

								path: { type: "string",
									description: "
										A JSON.path with the schema property as its root.  This indicates the part of 
										data.current.schema which had the invalid value. " },

								value: { type: "any",
									description: "The value of the property at data.current.path" } } },

						conflictsWith: { type: "object",
							properties: {
								schema: { type: "JSONSchema",
									description: "
										A schema, part of which precludes the possibility of data.current.path having
										the value it did." },

								path: { type: "string",
									description: "
										A JSON.path with data.current.schema as its root.  This indicates the part of
										data.conflictsWith.schema which data.current.path conflicted with" },

								value: { type: "any",
									description: 
										"The value at data.conflictsWith.path" } } } } } ] }
	*/
	InvalidValueError: function InvalidValueError(code, data) { this._ctor(code, data); this.name = "InvalidValueError"; },
	/**
		@interface {
			id: "JSON.schema.err.ErrorLocalizer",

			description: "
				A base class which can be used to get localized messages for 
				'JSON.schema.err.SchemaError's",

			parameters: [
				{ name: "lang", type: "string",
					description: "A language code indicting the language to be localized to"},

				{ name: "error", type: "JSON.schema.err.SchemaError",
					description: "The error to localize" } ],
			
			methods: {
				lang: { 
					description: "
						Used to get/set the localization language.
						If the lang parameter is omitted this will simply return the language code,
						otherwise it will set the language code and return the value.",

					parameters: [
						{ name: "lang", type: "string", optional: true,
							description: "A language code indicating the desired localization language"} ],

					returns: { type: "string", 
						description: "The language code which will be used after the call successfully completes" } },

				error: {
					description: "
						Used to get/set the error to localize.
						If the error parameter is omitted this will simply return the error to localize,
						otherwise it will set the error and return it.",

					parameters: [
						{ name: "error", type: "JSON.schema.err.SchemaError", optional: true,
							description: "An error to localize"} ],

					returns: { type: "JSON.schema.err.SchemaError",
						description: "The error which will be localized after the call successfully completes" } },

				message: {
					description: "Used to get the localized message for the error.",
					returns: { type: "string",
						description: "
							A message for the error provided localized to the language code provided 
							(assuming it exists in JSON.schema.err.messages).  Prior to returning the
							message it will be parsed for references to the properties of the current
							error.  The references must be of the form {<path>}, where <path> is a
							JSON.path to the property using the current error as the root.  So, for 
							example, in order to retrieve the current path from the data property of 
							the current error one could use {data.current.path}. The error code could 
							be retrieved using {code}.} } } }
	*/
	ErrorLocalizer: function ErrorLocalizer(lang, error) {
		this.errMsgs = JSON.schema.err.messages ? JSON.schema.err.messages : {};
		this.lng = typeof(lang) == "string" ? lang : "en";
		this.err = error instanceof JSON.schema.err.SchemaError ? error : null;
	},
	/**
		@interface {
			id: "JSON.schema.err.SubTypeErrorLocalizer",
			extends: "JSON.schema.err.ErrorLocalizer",

			description: "
				A subclass of JSON.schema.err.ErrorLocalizer which provides specialized
				localization of 'JSON.schema.err.SubTypeSchemaError's.  Using this localizer with a
				JSON.schema.err.SubTypeError will return a message with the causes as well, whereas
				JSON.schema.err.ErrorLocalizer will only return a message for the initial error" }
	*/
	SubTypeErrorLocalizer: function SubTypeErrorLocalizer(lang, error) {
		this._ctor(lang, error);
	},
	/**
		@interface {
			id: "JSON.schema.err.localize",

			description: "
				A function which automatically instantiates an appropriate localization class for 
				the error provided and returns a message for that error in the language specified",
			
			parameters: [
				{name: "lang", type: "string", 
					description: "A language code indicating the language to be localized to"},

				{name: "error", type: "JSON.schema.err.SchemaError",
					description: "The error instance to get a localized message for." } ] }
	*/
	localize: function jseLocalize(lang, error) {
		if (!("_sngltns" in JSON.schema.err)) { 
			JSON.schema.err._sngltns = {}; 
		}

		var fn = error.name + "Localizer";
		
		if (!(fn in JSON.schema.err._sngltns) && !(fn in JSON.schema.err)) { 
			fn = "ErrorLocalizer";
		}

		if (!(fn in JSON.schema.err._sngltns) && fn in JSON.schema.err) {
			JSON.schema.err._sngltns[fn] = new JSON.schema.err[fn](lang, error);
		}

		JSON.schema.err._sngltns[fn].error(error);
		JSON.schema.err._sngltns[fn].lang(lang);
		return JSON.schema.err._sngltns[fn].message();
	}
};
JSON.schema.err.SchemaError.prototype = new Error();
JSON.schema.err.SchemaError.prototype._ctor = JSON.schema.err.SchemaError;
JSON.schema.err.RequiredTypeError.prototype = new JSON.schema.err.SchemaError();
JSON.schema.err.TypeError.prototype = new JSON.schema.err.SchemaError();
JSON.schema.err.SubTypeError.prototype = new JSON.schema.err.SchemaError();
JSON.schema.err.IntroductionError.prototype = new JSON.schema.err.SchemaError();
JSON.schema.err.InvalidValueError.prototype = new JSON.schema.err.SchemaError();
JSON.schema.err.ErrorLocalizer.prototype = {
	_ctor: JSON.schema.err.ErrorLocalizer,
	lang: function jseefLang(lang) {
		if (lang && lang in this.errMsgs) { this.lng = lang; }
		return this.lng;
	},
	error: function jseefError(error) {
		if (error) { this.err = error; }
		return this.err;
	},
	message: function jseefMessage() {
		if (this.lng in this.errMsgs) {
			if (this.err.code in this.errMsgs[this.lng]) {
				var self = this;
				return this.errMsgs[this.lng][this.err.code].replace(
					new RegExp("{([^{]+)}","g"), 
					function ($0, $1) {
						var retVal = JSON.path(self.err, $1);
						return (retVal instanceof Array && retVal.length > 0) ? 
							retVal[0] : $1 + " not found"; 
					} 
				);
			} else {
				return "The code `" + 
					this.err.code + "' does not exist in `JSON.schema.err.messages." + 
					this.lng + "'.";
			}
		} else {
			return "The language `" + 
				this.lng + "' does not exist in `JSON.schema.err.messages'.";
		}
	}
};
JSON.schema.err.SubTypeErrorLocalizer.prototype = new JSON.schema.err.ErrorLocalizer();
JSON.schema.err.SubTypeErrorLocalizer.prototype._message = JSON.schema.err.SubTypeErrorLocalizer.prototype.message;
JSON.schema.err.SubTypeErrorLocalizer.prototype.message = function jsestefMessage() {
	var origError = this.err,
		causes = this.err.data.causedBy,
		msg = this._message(),
		steL = new JSON.schema.err.SubTypeErrorLocalizer(this.lang),
		i;

	for (i in causes) {
		if (!causes.hasOwnProperty(i)) {
			continue;
		}

		this.err = causes[i];
		if (causes[i] instanceof JSON.schema.err.SubTypeError) {
			steL.error(this.err);
			msg += "\n" + steL.message()
				.replace(new RegExp("\t", "g"), "\t\t")
				.replace(new RegExp("^"), "\t");
		} else {
			msg += "\n\t" + this._message();
		}
	}

	this.err = origError;
	
	return msg;
};